Repository navigation
Allow cancelling a pending video_player creation - #405
Conversation
1c51345 to
7b685d6
Compare
|
@cursor review |
Bugbot couldn't run — GitHub account mismatchThe GitHub account linked to your Cursor account does not match the PR author. Please ensure you're using the correct GitHub account, or run Bugbot from a team that covers this repository. |
wang-bin
left a comment
There was a problem hiding this comment.
Thanks for the clear write-up and the Windows repro — the diagnosis matches the video_player create/dispose coupling (dispose waits on _creatingCompleter, which only completes after createWithOptions returns).
The overall approach is good: Zone-scoped opt-in cancellation, awaitable disposeAsync(), skipping _videoSize on texture release, and a failed player id so initialize can finish. CI looks green.
Please address the inline notes before merge. Biggest ones: align MdkVideoPlayer with disposeAsync, and clarify/simplify the Future.any + StateError vs platform error path. Also worth documenting that this remains opt-in (default initialize/dispose still hangs on a stalled prepare()).
| // Resolve video_player's createWithOptions Future only after MDK has been | ||
| // stopped and deleted. Its controller can then safely finish dispose(). | ||
| await player.disposeAsync(); | ||
| player.streamCtl.close(); |
There was a problem hiding this comment.
disposeAsync() bypasses MdkVideoPlayer.dispose(), so streamCtl has to be closed here by hand.
Prefer closing the stream controller inside an overridden disposeAsync() on MdkVideoPlayer, and keep dispose() => unawaited(disposeAsync()) so both paths tear down the same way:
@override
Future<void> disposeAsync() async {
if (!streamCtl.isClosed) streamCtl.close();
_initialized = false;
await super.disposeAsync();
}
@override
void dispose() => unawaited(disposeAsync());Then this streamCtl.close() can go away.
There was a problem hiding this comment.
Done, I moved the stream cleanup into MdkVideoPlayer.disposeAsync() and kept dispose() => unawaited(disposeAsync()). I also guard late callbacks, including the textureSize continuation.
| (_) => throw StateError('FVP player creation cancelled')), | ||
| ]), | ||
| zoneValues: {_createCancellationZoneKey: this}, | ||
| ); |
There was a problem hiding this comment.
run() completes with StateError as soon as cancel() fires, while initialize() may still be running and later surfaces a PlatformException via the cancelled event stream.
Callers awaiting run() therefore see a different error than controller.value.
Consider either:
- Zone-wrap only (no
Future.anythrow) and let initialize fail through the normal platform error path, or - Keep the early completion but use one dedicated exception type and document that
initialize's later error is expected.
Also a short usage example in fvp.dart dartdoc would help, since this is opt-in.
There was a problem hiding this comment.
Thanks, I went with option 2. run() and the cancellation event now use FvpCreateCancelledException.
I kept the early completion because video_player can cancel its event subscription during dispose(), leaving the original initialize() future pending. The dartdoc explains the possible later error and that callers should await dispose() for native cleanup.
| player.streamCtl.close(); | ||
| final id = _nextFailureId--; | ||
| _cancelledEvents[id] = Stream<VideoEvent>.error(PlatformException( | ||
| code: 'creation cancelled', message: 'Playback changed')); |
There was a problem hiding this comment.
message: 'Playback changed' is a bit opaque (not a video_player constant I could find). Prefer something explicit like 'FVP player creation cancelled' so apps that surface errorDescription can tell cancel from a real media failure.
There was a problem hiding this comment.
I changed it to FVP player creation cancelled, using the same FvpCreateCancelledException.
| rethrow; | ||
| } | ||
| if (cancellation?.isCancelled ?? false) { | ||
| return _finishCancelledCreate(player); |
There was a problem hiding this comment.
Minor: prepare / textureSize are raced with _untilCancelled, but updateTexture / createTexture only check isCancelled after the await (and in catch). Cancel during texture creation is unlikely to hang like network prepare, but the handling is inconsistent — wrapping with _untilCancelled would match the earlier waits.
There was a problem hiding this comment.
I kept this await because createTexture() is still using the native player handle. Racing it with cancellation would let _finishCancelledCreate() delete the player before texture creation returns.
Once allocation completes, I release the texture before deleting the player. I tested cancellation with allocation delayed by 0, 20, 200 and 1000 ms on Windows and macOS; all textures were released.
| if (dart.library.html) 'src/video_player_dummy.dart'; | ||
|
|
||
| export 'src/controller.dart'; | ||
| export 'src/create_cancellation.dart' show FvpCreateCancellation; |
There was a problem hiding this comment.
Please add a short usage example here (or on FvpCreateCancellation) so callers know this is opt-in and that cancel() must be called before dispose() when initialize may still be pending:
final cancel = FvpCreateCancellation();
try {
await cancel.run(() => controller.initialize());
} finally {
cancel.cancel();
await controller.dispose();
}There was a problem hiding this comment.
Added to the FvpCreateCancellation dartdoc, with cancel() before dispose() and a note that this is opt-in.
f462191 to
086395c
Compare
|
Thanks for the review! I've updated the patch in 086395c and replied inline. I also updated the test results in the PR description. |
wang-bin
left a comment
There was a problem hiding this comment.
Re-reviewed 086395c — previous notes are addressed. LGTM.
MdkVideoPlayer.disposeAsync()now ownsstreamCtlcleanup; late callbacks guardstreamCtl.isClosed- Shared
FvpCreateCancelledExceptionforrun()and the failed-create event stream; dartdoc covers the early/initializedual path and opt-in usage - Keeping the
updateTextureawait (then release in dispose) is the right call vs racing delete against an in-flightcreateTexture; thanks for the delayed-allocation tests on Windows/macOS - Platform
dispose()awaitingdisposeAsync()is a good tightening of the normal path too
CI is green on this head.
|
I add isClosed checks in master branch, can you rebase? |
086395c to
681c64e
Compare
|
i think you can split the patch into 3 commits like this:
|
Player.dispose() was fire-and-forget, so VideoPlayerPlatform.dispose() returned before native teardown finished, and a texture release error escaped as an uncaught async error. Add Player.disposeAsync() and keep dispose() as an unawaited wrapper. MdkVideoPlayer overrides it to close its event stream first, and MdkVideoPlayerPlatform.dispose() awaits it. If the release fails, the player is kept so that dispose() can be retried. Releasing a texture no longer waits for the video size. That future never completes while a load is stalled, so disposal could hang there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If texture creation threw, create() threw too. video_player then never got a player ID, and VideoPlayerController.dispose() kept waiting for the creation to finish. Return a failed player ID instead. Its event stream reports the error, so initialize() fails as usual, and dispose(id) releases the player kept under that ID. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If a source stalls in MDK prepare(), createWithOptions() stays pending and VideoPlayerController.dispose() waits for it forever. FvpCreateCancellation is an opt-in zone scope for initialize(). Calling cancel() before dispose() stops waiting for preparation and video size, deletes the pending player and returns a failed ID, so video_player can finish disposal. run() and the event stream both report FvpCreateCancelledException. A texture allocation already in flight is awaited, then released before the player is deleted. Without the scope, a stalled preparation still blocks disposal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
681c64e to
c7cc94d
Compare
|
Done, split into three commits as you suggested:
The final code is identical to 681c64e. Each commit is formatted and passes analysis; the lifecycle and disposal tests pass on each commit, and all 18 Dart tests on the last one. |
|
nice |
Summary
If a source stalls in MDK
prepare(),createWithOptions()remains pending andVideoPlayerController.dispose()cannot release the player. I reproduced this with a localhost response delayed by 20 seconds on Windows and macOS Release.This adds an opt-in
FvpCreateCancellationscope forinitialize(). Callingcancel()beforedispose()lets FVP stop and delete the pending player, then return a failed ID sovideo_playercan finish disposal. Cancellation usesFvpCreateCancelledExceptionin bothrun()and the event stream. The dartdoc includes a usage example. Without the scope, stalled preparation still blocks disposal.Player.disposeAsync()makes teardown awaitable while keepingvoid dispose().MdkVideoPlayer.disposeAsync()closes the event stream and ignores late callbacks. Texture release skips the video-size wait. I kept texture allocation awaited: deleting the player whilecreateTexture()still uses its native handle would be unsafe.Texture creation and cleanup errors also return a failed ID, avoiding another stuck controller. Platform disposal reports release errors and retains the player for retry while its texture remains registered.
Testing
example/PENDING_CREATE_REPRO.md. It uses a delayed localhost response and writes MDK logs atLevel.ALL. Initial Windows logs at 7b685d6: without cancellation,dispose()timed out after 8 seconds; with cancellation, it completed in 38 ms.a233309.VideoPlayerPlatform.dispose(id)completes cleanup. Cancellation and normal playback are covered. Holding native texture release confirms that controller disposal waits for it.a233309with the same MDK 0.39.0 and FFmpeg libraries.callbacks.cpploader shim; Release tests use the real plugin. Windows and macOS CI builds pass on086395c.The macOS retest with MDK 0.39.0 is still unconfirmed because I lost access to the test machine. I also reproduced an intermittent parallel-player teardown hang on unpatched
fc97343with the same MDK and native stack offsets. It did not occur in the completed Mac run; I haven't identified its trigger.